using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;


namespace LightCAD.Three
{
    public class CylinderGeometry : BufferGeometry
    {
        public class Parameters
        {
            public double radiusTop;
            public double radiusBottom;
            public double height;
            public int radialSegments;
            public int heightSegments;
            public bool openEnded;
            public double thetaStart;
            public double thetaLength;
        }
        #region Properties

        public Parameters parameters;

        #endregion

        #region constructor
        public CylinderGeometry(double radiusTop = 1, double radiusBottom = 1, double height = 1, int radialSegments = 32, int heightSegments = 1, bool openEnded = false, double thetaStart = 0, double thetaLength = Math.PI * 2)
        {
            this.type = "CylinderGeometry";
            this.parameters = new Parameters
            {
                radiusTop = radiusTop,
                radiusBottom = radiusBottom,
                height = height,
                radialSegments = radialSegments,
                heightSegments = heightSegments,
                openEnded = openEnded,
                thetaStart = thetaStart,
                thetaLength = thetaLength

            };
            var scope = this;
            //radialSegments = (int)Math.Floor(radialSegments);
            //heightSegments = (int)Math.Floor(heightSegments);
            // buffers
            var indices = new ListEx<int>();
            var vertices = new ListEx<double>();
            var normals = new ListEx<double>();
            var uvs = new ListEx<double>();
            // helper variables
            var index = 0;
            var indexArray = new ListEx<ListEx<int>>();
            var halfHeight = height / 2;
            var groupStart = 0;
            // generate geometry
            generateTorso();
            if (openEnded == false)
            {
                if (radiusTop > 0) generateCap(true);
                if (radiusBottom > 0) generateCap(false);
            }
            // build geometry
            this.setIndex(indices.ToArray());
            this.setAttribute("position", new Float32BufferAttribute(vertices.ToArray(), 3));
            this.setAttribute("normal", new Float32BufferAttribute(normals.ToArray(), 3));
            this.setAttribute("uv", new Float32BufferAttribute(uvs.ToArray(), 2));

            void generateTorso()
            {
                var normal = new Vector3();
                var vertex = new Vector3();
                var groupCount = 0;
                // this will be used to calculate the normal
                var slope = (radiusBottom - radiusTop) / height;
                // generate vertices, normals and uvs
                for (int y = 0; y <= heightSegments; y++)
                {
                    var indexRow = new ListEx<int>();
                    var v = (double)y / heightSegments;
                    // calculate the radius of the current row
                    var radius = v * (radiusBottom - radiusTop) + radiusTop;
                    for (int x = 0; x <= radialSegments; x++)
                    {
                        var u = (double)x / radialSegments;
                        var theta = u * thetaLength + thetaStart;
                        var sinTheta = Math.Sin(theta);
                        var cosTheta = Math.Cos(theta);
                        // vertex
                        vertex.X = radius * sinTheta;
                        vertex.Y = -v * height + halfHeight;
                        vertex.Z = radius * cosTheta;
                        vertices.Push(vertex.X, vertex.Y, vertex.Z);
                        // normal
                        normal.Set(sinTheta, slope, cosTheta).Normalize();
                        normals.Push(normal.X, normal.Y, normal.Z);
                        // uv
                        uvs.Push(u, 1 - v);
                        // save index of vertex in respective row
                        indexRow.Push(index++);
                    }
                    // now save vertices of the row in our index array
                    indexArray.Push(indexRow);
                }
                // generate indices
                for (int x = 0; x < radialSegments; x++)
                {
                    for (int y = 0; y < heightSegments; y++)
                    {
                        // we use the index array to access the correct indices
                        var a = indexArray[y][x];
                        var b = indexArray[y + 1][x];
                        var c = indexArray[y + 1][x + 1];
                        var d = indexArray[y][x + 1];
                        // faces
                        indices.Push(a, b, d);
                        indices.Push(b, c, d);
                        // update group counter
                        groupCount += 6;
                    }
                }
                // add a group to the geometry. this will ensure multi material support
                scope.addGroup(groupStart, groupCount, 0);
                // calculate new start value for groups
                groupStart += groupCount;
            }
            void generateCap(bool top)
            {
                // save the index of the first center vertex
                var centerIndexStart = index;
                var uv = new Vector2();
                var vertex = new Vector3();
                var groupCount = 0;
                var radius = top ? radiusTop : radiusBottom;
                var sign = top ? 1 : -1;
                // first we generate the center vertex data of the cap.
                // because the geometry needs one set of uvs per face,
                // we must generate a center vertex per face/segment
                for (int x = 1; x <= radialSegments; x++)
                {
                    // vertex
                    vertices.Push(0, halfHeight * sign, 0);
                    // normal
                    normals.Push(0, sign, 0);
                    // uv
                    uvs.Push(0.5, 0.5);
                    // increase index
                    index++;
                }
                // save the index of the last center vertex
                var centerIndexEnd = index;
                // now we generate the surrounding vertices, normals and uvs
                for (int x = 0; x <= radialSegments; x++)
                {
                    var u = (double)x / radialSegments;
                    var theta = u * thetaLength + thetaStart;
                    var cosTheta = Math.Cos(theta);
                    var sinTheta = Math.Sin(theta);
                    // vertex
                    vertex.X = radius * sinTheta;
                    vertex.Y = halfHeight * sign;
                    vertex.Z = radius * cosTheta;
                    vertices.Push(vertex.X, vertex.Y, vertex.Z);
                    // normal
                    normals.Push(0, sign, 0);
                    // uv
                    uv.X = (cosTheta * 0.5) + 0.5;
                    uv.Y = (sinTheta * 0.5 * sign) + 0.5;
                    uvs.Push(uv.X, uv.Y);
                    // increase index
                    index++;
                }
                // generate indices
                for (int x = 0; x < radialSegments; x++)
                {
                    var c = centerIndexStart + x;
                    var i = centerIndexEnd + x;
                    if (top)
                    {
                        // face top
                        indices.Push(i, i + 1, c);
                    }
                    else
                    {
                        // face bottom
                        indices.Push(i + 1, i, c);
                    }
                    groupCount += 3;
                }
                // add a group to the geometry. this will ensure multi material support
                scope.addGroup(groupStart, groupCount, top ? 1 : 2);
                // calculate new start value for groups
                groupStart += groupCount;
            }
        }
        #endregion

        #region methods
        public CylinderGeometry copy(CylinderGeometry source)
        {
            base.copy(source);
            this.parameters = new Parameters()
            {
                radiusTop = source.parameters.radiusTop,
                radiusBottom = source.parameters.radiusBottom,
                height = source.parameters.height,
                radialSegments = source.parameters.radialSegments,
                heightSegments = source.parameters.heightSegments,
                openEnded = source.parameters.openEnded,
                thetaStart = source.parameters.thetaStart,
                thetaLength = source.parameters.thetaLength,
            };
            return this;
        }

        #endregion
    }
}
